//
//  ========================================================================
//  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//


package org.eclipse.jetty.http2.hpack;

import java.nio.ByteBuffer;

import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * Hpack Decoder
 * <p>This is not thread safe and may only be called by 1 thread at a time.</p>
 */
public class HpackDecoder
{
    public static final Logger LOG = Log.getLogger(HpackDecoder.class);
    public final static HttpField.LongValueHttpField CONTENT_LENGTH_0 =
            new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH,0L);

    private final HpackContext _context;
    private final MetaDataBuilder _builder;
    private int _localMaxDynamicTableSize;

    /**
     * @param localMaxDynamicTableSize  The maximum allowed size of the local dynamic header field table.
     * @param maxHeaderSize The maximum allowed size of a headers block, expressed as total of all name and value characters.
     */
    public HpackDecoder(int localMaxDynamicTableSize, int maxHeaderSize)
    {
        _context=new HpackContext(localMaxDynamicTableSize);
        _localMaxDynamicTableSize=localMaxDynamicTableSize;
        _builder = new MetaDataBuilder(maxHeaderSize);
    }

    public HpackContext getHpackContext()
    {
        return _context;
    }

    public void setLocalMaxDynamicTableSize(int localMaxdynamciTableSize)
    {
        _localMaxDynamicTableSize=localMaxdynamciTableSize;
    }

    public MetaData decode(ByteBuffer buffer)
    {
        if (LOG.isDebugEnabled())
            LOG.debug(String.format("CtxTbl[%x] decoding %d octets",_context.hashCode(),buffer.remaining()));

        // If the buffer is big, don't even think about decoding it
        if (buffer.remaining()>_builder.getMaxSize())
            throw new BadMessageException(HttpStatus.REQUEST_ENTITY_TOO_LARGE_413,"Header frame size "+buffer.remaining()+">"+_builder.getMaxSize());


        while(buffer.hasRemaining())
        {
            if (LOG.isDebugEnabled())
            {
                int l=Math.min(buffer.remaining(),16);
                // TODO: not guaranteed the buffer has a backing array !
                LOG.debug("decode {}{}",
                        TypeUtil.toHexString(buffer.array(),buffer.arrayOffset()+buffer.position(),l),
                        l<buffer.remaining()?"...":"");
            }

            byte b = buffer.get();
            if (b<0)
            {
                // 7.1 indexed if the high bit is set
                int index = NBitInteger.decode(buffer,7);
                Entry entry=_context.get(index);
                if (entry==null)
                {
                    throw new BadMessageException("Unknown index "+index);
                }
                else if (entry.isStatic())
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("decode IdxStatic {}",entry);
                    // emit field
                    _builder.emit(entry.getHttpField());

                    // TODO copy and add to reference set if there is room
                    // _context.add(entry.getHttpField());
                }
                else
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("decode Idx {}",entry);
                    // emit
                    _builder.emit(entry.getHttpField());
                }
            }
            else
            {
                // look at the first nibble in detail
                byte f= (byte)((b&0xF0)>>4);
                String name;
                HttpHeader header;
                String value;

                boolean indexed;
                int name_index;

                switch (f)
                {
                    case 2: // 7.3
                    case 3: // 7.3
                        // change table size
                        int size = NBitInteger.decode(buffer,5);
                        if (LOG.isDebugEnabled())
                            LOG.debug("decode resize="+size);
                        if (size>_localMaxDynamicTableSize)
                            throw new IllegalArgumentException();
                        _context.resize(size);
                        continue;

                    case 0: // 7.2.2
                    case 1: // 7.2.3
                        indexed=false;
                        name_index=NBitInteger.decode(buffer,4);
                        break;


                    case 4: // 7.2.1
                    case 5: // 7.2.1
                    case 6: // 7.2.1
                    case 7: // 7.2.1
                        indexed=true;
                        name_index=NBitInteger.decode(buffer,6);
                        break;

                    default:
                        throw new IllegalStateException();
                }


                boolean huffmanName=false;

                // decode the name
                if (name_index>0)
                {
                    Entry name_entry=_context.get(name_index);
                    name=name_entry.getHttpField().getName();
                    header=name_entry.getHttpField().getHeader();
                }
                else
                {
                    huffmanName = (buffer.get()&0x80)==0x80;
                    int length = NBitInteger.decode(buffer,7);
                    _builder.checkSize(length,huffmanName);
                    if (huffmanName)
                        name=Huffman.decode(buffer,length);
                    else
                        name=toASCIIString(buffer,length);
                    for (int i=0;i<name.length();i++)
                    {
                        char c=name.charAt(i);
                        if (c>='A'&&c<='Z')
                        {
                            throw new BadMessageException(400,"Uppercase header name");
                        }
                    }
                    header=HttpHeader.CACHE.get(name);
                }

                // decode the value
                boolean huffmanValue = (buffer.get()&0x80)==0x80;
                int length = NBitInteger.decode(buffer,7);
                _builder.checkSize(length,huffmanValue);
                if (huffmanValue)
                    value=Huffman.decode(buffer,length);
                else
                    value=toASCIIString(buffer,length);

                // Make the new field
                HttpField field;
                if (header==null)
                {
                    // just make a normal field and bypass header name lookup
                    field = new HttpField(null,name,value);
                }
                else
                {
                    // might be worthwhile to create a value HttpField if it is indexed
                    // and/or of a type that may be looked up multiple times.
                    switch(header)
                    {
                        case C_STATUS:
                            if (indexed)
                                field = new HttpField.IntValueHttpField(header,name,value);
                            else
                                field = new HttpField(header,name,value);
                            break;

                        case C_AUTHORITY:
                            field = new AuthorityHttpField(value);
                            break;

                        case CONTENT_LENGTH:
                            if ("0".equals(value))
                                field = CONTENT_LENGTH_0;
                            else
                                field = new HttpField.LongValueHttpField(header,name,value);
                            break;

                        default:
                            field = new HttpField(header,name,value);
                            break;
                    }
                }

                if (LOG.isDebugEnabled())
                {
                    LOG.debug("decoded '{}' by {}/{}/{}",
                            field,
                            name_index > 0 ? "IdxName" : (huffmanName ? "HuffName" : "LitName"),
                            huffmanValue ? "HuffVal" : "LitVal",
                            indexed ? "Idx" : "");
                }

                // emit the field
                _builder.emit(field);

                // if indexed
                if (indexed)
                {
                    // add to dynamic table
                    _context.add(field);
                }

            }
        }

        return _builder.build();
    }

    public static String toASCIIString(ByteBuffer buffer,int length)
    {
        StringBuilder builder = new StringBuilder(length);
        int position=buffer.position();
        int start=buffer.arrayOffset()+ position;
        int end=start+length;
        buffer.position(position+length);
        byte[] array=buffer.array();
        for (int i=start;i<end;i++)
            builder.append((char)(0x7f&array[i]));
        return builder.toString();
    }

    @Override
    public String toString()
    {
        return String.format("HpackDecoder@%x{%s}",hashCode(),_context);
    }
}
